-- Copyright (c) 2025 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at:
--         http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.
-- Description:
--   This class is responsible for canbus collection, specified functions are following:
--   1. Management for all canbus instances
--   2. Functional interfaces of canbus collection, such as batch sending, or statistics

local log = require 'mc.logging'
local class = require 'mc.class'
local utils = require 'mc.utils'
local singleton = require 'mc.singleton'
local file_checker = require 'utils.file'
local worker = require 'worker.core'
local canbus_instance = require 'canbus.instance'
local canbus_dispatcher = require 'canbus.dispatcher'

local canbus_collection = class()

function canbus_collection:ctor()
    self.canbus_enabled = false
    self.collections = {}
    self.trans_id_maps = {}
end

function canbus_collection:get_metrics(channel)
    local all = channel == 0xFF
    local metrics = {}
    for k, v in pairs(self.collections) do
        if not all and k ~= channel then
            goto continue
        end
        table.insert(metrics, {
            channel = k,
            recv_count = v.recv_count,
            send_count = v.send_count,
            send_failed = v.send_failed
        })
        ::continue::
    end
    return metrics
end

function canbus_collection:initialize_canbus(id, channels)
    if not id or id == 0xFF then
        log:error('canbus initialize failed, error: invalid identifier')
        return false
    end

    if not channels or type(channels) ~= 'table' or not next(channels) then
        log:error('canbus initialize failed, error: invalid channels')
        return false
    end

    local available = false
    for _, v in pairs(channels) do
        local instance = canbus_instance.new(id, v)
        instance:init(v)
        available = true
        self.collections[v] = instance
    end
    if not available then
        log:error('canbus initialize failed, error: all channels are unavailable')
        self.canbus_enabled = false
        return
    end
    self.canbus_enabled = true
    self.identifier = id
end

function canbus_collection:listen()
    if not self.canbus_enabled then
        log:warn('canbus is disabled and cannot recv msg')
        return false
    end

    -- initialize the canbus message dispatcher
    canbus_dispatcher.get_instance():initialize()

    -- start listening task worker listen on each channel in collections 
    for k, v in pairs(self.collections) do
        v.recv_worker = worker.new(1)
        v.recv_worker:start_module('canbus.receiver')
        v.recv_worker:send(k)
    end
    return true
end

-- register object discovery callbacks
function canbus_collection:register(bus, mgmt_obj)
    if not mgmt_obj.Enabled then
        log:notice('can management is disabled, status [%s]', tostring(mgmt_obj.Enabled))
        self.canbus_enabled = false
        return
    end

    -- initialize canbus handler and driver
    self:initialize_canbus(mgmt_obj.Identifier, mgmt_obj.Channels)

    -- start canbus receiver and listen message
    self:listen()
end

-- close all canbus in collections
function canbus_collection:close()
    for _, v in pairs(self.collections) do
        v.recv_worker:stop()
        v.driver:unlock()
        v.driver:free()
    end
end

-- reset specified canbus in collections, reset all channels while channel is in default
function canbus_collection:reset(channel)
    local all = not channel or channel == 0xFF
    if not all and not self.collections[channel] then
        log:warn('canbus [%s] is missed in collections', channel)
        return
    end

    for k, v in pairs(self.collections) do
        if not all and k ~= channel then
            goto continue
        end
        v.driver:unlock()
        v.driver:reset()
        log:notice('canbus [%s] is reset', channel)
        ::continue::
    end
end

-- generate unique transmission id
-- NOTE: transmission id will be unique on all canbus channels, incase of that the channel transmit frame X 
-- and has exception occurs, which transfer frames left to another channel to transmit
local g_trans_id = 0
local function generate_trans_id()
    g_trans_id = g_trans_id + 1
    if g_trans_id > 0xFF then
        log:notice('canbus transmission id is reset.')
        g_trans_id = 1
    end
    return g_trans_id
end

-- send message via specified canbus in collections
function canbus_collection:send(msg, channel, dest)
    if not self.canbus_enabled then
        log:warn('canbus is disabled and cannot send msg')
        return false
    end

    local all = not channel or channel == 0xFF
    if not all and not self.collections[channel] then
        log:warn('canbus [%s] is missed in collections', channel)
        return false
    end

    -- send message via specified channel
    local trans_id = generate_trans_id()
    local ok = false
    for k, v in pairs(self.collections) do
        if not all and k ~= channel then
            goto continue
        end
        ok = ok or v:send(msg, trans_id, dest)
        ::continue::
    end
    if not ok then
        log:error('message send failed via can[%s]', channel)
        return false
    end

    -- send message successfully as long as a channel succeed to send
    return true, trans_id
end

function canbus_collection:dump_canbus(path)
    -- makeup dump content
    local content = {}
    if not self.canbus_enabled then
        table.insert(content, 'canbus is not enabled.\n')
    else
        -- makeup the title
        local items = {}
        local format = {'%-10s', '%-12s', '%-12s', '%-20s'}
        local title = {'Channel', 'Send Count', 'Recv Count', 'Send Failed Count'}
        for i =  1, #format do
            table.insert(items, string.format(format[i], title[i]))
        end
        table.insert(content, table.concat(items, '|'))

        -- makeup the content
        local metrics = self:get_metrics(0xFF)
        for _, v in ipairs(metrics) do
            items = {v.channel, v.send_count, v.recv_count, v.send_failed}
            for i = 1, #format do
                items[i] = string.format(format[i], items[i])
            end
            table.insert(content, table.concat(items, '|'))
        end
    end

    -- dump content into file
    local fd = file_checker.open_s(path .. '/canbus_metrics.txt', 'w')
    if not fd then
        log:error('dump canbus failed, because file open failed')
        return
    end
    utils.safe_close_file(fd, fd.write, fd, table.concat(content, '\n'))
    utils.chmod(path, utils.S_IRUSR | utils.S_IWUSR | utils.S_IRGRP)
end

return singleton(canbus_collection)